#include "uc8151_legacy.hpp"

#include <cstdlib>
#include <math.h>

namespace pimoroni {

  enum reg {
    PSR      = 0x00,
    PWR      = 0x01,
    POF      = 0x02,
    PFS      = 0x03,
    PON      = 0x04,
    PMES     = 0x05,
    BTST     = 0x06,
    DSLP     = 0x07,
    DTM1     = 0x10,
    DSP      = 0x11,
    DRF      = 0x12,
    DTM2     = 0x13,
    LUT_VCOM = 0x20,
    LUT_WW   = 0x21,
    LUT_BW   = 0x22,
    LUT_WB   = 0x23,
    LUT_BB   = 0x24,
    PLL      = 0x30,
    TSC      = 0x40,
    TSE      = 0x41,
    TSR      = 0x43,
    TSW      = 0x42,
    CDI      = 0x50,
    LPD      = 0x51,
    TCON     = 0x60,
    TRES     = 0x61,
    REV      = 0x70,
    FLG      = 0x71,
    AMV      = 0x80,
    VV       = 0x81,
    VDCS     = 0x82,
    PTL      = 0x90,
    PTIN     = 0x91,
    PTOU     = 0x92,
    PGM      = 0xa0,
    APG      = 0xa1,
    ROTP     = 0xa2,
    CCSET    = 0xe0,
    PWS      = 0xe3,
    TSSET    = 0xe5
  };

  bool UC8151_Legacy::is_busy() {
    return !gpio_get(BUSY);
  }

  void UC8151_Legacy::busy_wait() {
    while(is_busy()) {
      tight_loop_contents();
    }
  }

  void UC8151_Legacy::reset() {
    gpio_put(RESET, 0); sleep_ms(10);
    gpio_put(RESET, 1); sleep_ms(10);
    busy_wait();
  }

  void UC8151_Legacy::default_luts() {
    command(LUT_VCOM, {
      0x00, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x00, 0x8c, 0x8c, 0x00, 0x00, 0x04,
      0x00, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00
    });

    command(LUT_WW, {
      0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
      0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BW, {
      0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
      0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_WB, {
      0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
      0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BB, {
      0xa8, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04,
      0x54, 0x64, 0x64, 0x37, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    busy_wait();
  }

  void UC8151_Legacy::medium_luts() {

    command(LUT_VCOM, {
      0x00, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x00, 0x23, 0x23, 0x00, 0x00, 0x02,
      0x00, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00
    });

    command(LUT_WW, {
      0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
      0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BW, {
      0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
      0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_WB, {
      0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
      0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BB, {
      0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x60, 0x23, 0x23, 0x00, 0x00, 0x02,
      0x54, 0x16, 0x16, 0x0d, 0x00, 0x01,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    busy_wait();
  }

  void UC8151_Legacy::fast_luts() {
    // 0x3c, 0x00, 0x2b, 0x2b, 0x24, 0x1a, ????
    command(LUT_VCOM, {
      0x00, 0x04, 0x04, 0x07, 0x00, 0x01,
      0x00, 0x0c, 0x0c, 0x00, 0x00, 0x02,
      0x00, 0x04, 0x04, 0x07, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00
    });

    command(LUT_WW, {
      0x54, 0x04, 0x04, 0x07, 0x00, 0x01,
      0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
      0xa8, 0x04, 0x04, 0x07, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BW, {
      0x54, 0x04, 0x04, 0x07, 0x00, 0x01,
      0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
      0xa8, 0x04, 0x04, 0x07, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_WB, {
      0xa8, 0x04, 0x04, 0x07, 0x00, 0x01,
      0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
      0x54, 0x04, 0x04, 0x07, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BB, {
      0xa8, 0x04, 0x04, 0x07, 0x00, 0x01,
      0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02,
      0x54, 0x04, 0x04, 0x07, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(PLL, {
      HZ_200
    });

    busy_wait();
  }

  void UC8151_Legacy::turbo_luts() {
    // 0x3c, 0x00, 0x2b, 0x2b, 0x24, 0x1a, ????
    command(LUT_VCOM, {
      0x00, 0x01, 0x01, 0x02, 0x00, 0x01,
      0x00, 0x02, 0x02, 0x00, 0x00, 0x02,
      0x00, 0x02, 0x02, 0x03, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00
    });

    command(LUT_WW, {
      0x54, 0x01, 0x01, 0x02, 0x00, 0x01,
      0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
      0xa8, 0x02, 0x02, 0x03, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BW, {
      0x54, 0x01, 0x01, 0x02, 0x00, 0x01,
      0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
      0xa8, 0x02, 0x02, 0x03, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_WB, {
      0xa8, 0x01, 0x01, 0x02, 0x00, 0x01,
      0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
      0x54, 0x02, 0x02, 0x03, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(LUT_BB, {
      0xa8, 0x01, 0x01, 0x02, 0x00, 0x01,
      0x60, 0x02, 0x02, 0x00, 0x00, 0x02,
      0x54, 0x02, 0x02, 0x03, 0x00, 0x02,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    });

    command(PLL, {
      HZ_200
    });

    busy_wait();
  }

  void UC8151_Legacy::init() {
    // configure spi interface and pins
    spi_init(spi, 12'000'000);

    gpio_set_function(DC, GPIO_FUNC_SIO);
    gpio_set_dir(DC, GPIO_OUT);

    gpio_set_function(CS, GPIO_FUNC_SIO);
    gpio_set_dir(CS, GPIO_OUT);
    gpio_put(CS, 1);

    gpio_set_function(RESET, GPIO_FUNC_SIO);
    gpio_set_dir(RESET, GPIO_OUT);
    gpio_put(RESET, 1);

    gpio_set_function(BUSY, GPIO_FUNC_SIO);
    gpio_set_dir(BUSY, GPIO_IN);
    gpio_set_pulls(BUSY, true, false);

    gpio_set_function(SCK,  GPIO_FUNC_SPI);
    gpio_set_function(MOSI, GPIO_FUNC_SPI);

    setup();
  };

  void UC8151_Legacy::setup(uint8_t speed) {
    reset();

    _update_speed = speed;

    if(speed == 0) {
      command(PSR, {
        RES_128x296 | LUT_OTP | FORMAT_BW | SHIFT_RIGHT | BOOSTER_ON | RESET_NONE
      });
    } else {
      command(PSR, {
        RES_128x296 | LUT_REG | FORMAT_BW | SHIFT_RIGHT | BOOSTER_ON | RESET_NONE
      });
    }
    switch(speed) {
      case 0:
        // Note: the defult luts are built in so we don't really need to flash them here
        // they are preserved above for posterity and reference mostly.
        break;
      case 1:
        medium_luts();
        break;
      case 2:
        fast_luts();
        break;
      case 3:
        turbo_luts();
        break;
      default:
        break;
    }

    command(PWR, {
      VDS_INTERNAL | VDG_INTERNAL,
      VCOM_VD | VGHL_16V,
      0b101011,
      0b101011,
      0b101011
    });

    command(PON); // power on
    busy_wait();

    // booster soft start configuration
    command(BTST, {
      START_10MS | STRENGTH_3 | OFF_6_58US,
      START_10MS | STRENGTH_3 | OFF_6_58US,
      START_10MS | STRENGTH_3 | OFF_6_58US
    });

    command(PFS, {
      FRAMES_1
    });

    command(TSE, {
      TEMP_INTERNAL | OFFSET_0
    });

    command(TCON, {0x22}); // tcon setting
    command(CDI, {(uint8_t)(inverted ? 0b01'01'1100 : 0b01'00'1100)}); // vcom and data interval

    command(PLL, {
      HZ_100
    });

    command(POF);
    busy_wait();
  }

  void UC8151_Legacy::power_off() {
    command(POF);
  }

  void UC8151_Legacy::read(uint8_t reg, size_t len, uint8_t *data) {
    gpio_put(CS, 0);

    gpio_put(DC, 0); // command mode
    spi_write_blocking(spi, &reg, 1);

    if(len > 0) {
      gpio_put(DC, 1); // data mode
      gpio_set_function(SCK,  GPIO_FUNC_SIO);
      gpio_set_dir(SCK, GPIO_OUT);
      gpio_set_function(MOSI, GPIO_FUNC_SIO);
      gpio_set_dir(MOSI, GPIO_IN);
      for(auto i = 0u; i < len; i++) {
        int byte = i / 8;
        int bit = i % 8;
        gpio_put(SCK, true);
        bool value = gpio_get(MOSI);
        data[byte] |= value << (7-bit);
        gpio_put(SCK, false);
      }

      gpio_set_function(SCK,  GPIO_FUNC_SPI);
      gpio_set_function(MOSI, GPIO_FUNC_SPI);
    }

    gpio_put(CS, 1);
  }

  void UC8151_Legacy::command(uint8_t reg, size_t len, const uint8_t *data) {
    gpio_put(CS, 0);

    gpio_put(DC, 0); // command mode
    spi_write_blocking(spi, &reg, 1);

    if(len > 0) {
      gpio_put(DC, 1); // data mode
      spi_write_blocking(spi, (const uint8_t*)data, len);
    }

    gpio_put(CS, 1);
  }

  void UC8151_Legacy::data(size_t len, const uint8_t *data) {
    gpio_put(CS, 0);
    gpio_put(DC, 1); // data mode
    spi_write_blocking(spi, (const uint8_t*)data, len);
    gpio_put(CS, 1);
  }

  void UC8151_Legacy::command(uint8_t reg, std::initializer_list<uint8_t> values) {
    command(reg, values.size(), (uint8_t *)values.begin());
  }

  void UC8151_Legacy::pixel(int x, int y, int v) {
    // bounds check
    if(x < 0 || y < 0 || x >= width || y >= height) return;

    // pointer to byte in framebuffer that contains this pixel
    uint8_t *p = &frame_buffer[(y / 8) + (x * (height / 8))];

    uint8_t  o = 7 - (y & 0b111);       // bit offset within byte
    uint8_t  m = ~(1 << o);             // bit mask for byte
    uint8_t  b = (v == 0 ? 0 : 1) << o; // bit value shifted to position

    *p &= m; // clear bit
    *p |= b; // set bit value
  }

  uint8_t* UC8151_Legacy::get_frame_buffer() {
	  return frame_buffer;
  }

  void UC8151_Legacy::invert(bool inv) {
    inverted = inv;
    command(CDI, {(uint8_t)(inverted ? 0b01'01'1100 : 0b01'00'1100)}); // vcom and data interval
  }

  void UC8151_Legacy::update_speed(uint8_t speed) {
    setup(speed);
  }

  uint8_t UC8151_Legacy::update_speed() {
    return _update_speed;
  }

  uint32_t UC8151_Legacy::update_time() {
    switch(_update_speed) {
      case 0:
        return 4500;
      case 1:
        return 2000;
      case 2:
        return 800;
      case 3:
        return 250;
      default:
        return 4500;
    }
  }

  void UC8151_Legacy::partial_update(int x, int y, int w, int h, bool blocking) {
    // y is given in columns ("banks"), which are groups of 8 horiontal pixels
    // x is given in pixels
    if(blocking) {
      busy_wait();
    }

    int cols = h / 8;
    int y1 = y / 8;
    //int y2 = y1 + cols;

    int rows = w;
    int x1 = x;
    //int x2 = x + rows;

    uint8_t partial_window[7] = {
      (uint8_t)(y),
      (uint8_t)(y + h - 1),
      (uint8_t)(x >> 8),
      (uint8_t)(x & 0xff),
      (uint8_t)((x + w - 1) >> 8),
      (uint8_t)((x + w - 1) & 0xff),
      0b00000001  // PT_SCAN
    };
    command(PON); // turn on

    command(PTIN); // enable partial mode
    command(PTL, sizeof(partial_window), partial_window);

    command(DTM2);
    for (auto dx = 0; dx < rows; dx++) {
      int sx = dx + x1;
      int sy = y1;
      data(cols, &frame_buffer[sy + (sx * (height / 8))]);
    }
    command(DSP); // data stop

    command(DRF); // start display refresh

    if(blocking) {
      busy_wait();

      command(POF); // turn off
    }
  }

  void UC8151_Legacy::update(bool blocking) {
    if(blocking) {
      busy_wait();
    }

    command(PON); // turn on

    command(PTOU); // disable partial mode

    command(DTM2, (width * height) / 8, frame_buffer); // transmit framebuffer
    command(DSP); // data stop

    command(DRF); // start display refresh

    if(blocking) {
      busy_wait();

      command(POF); // turn off
    }
  }

  void UC8151_Legacy::off() {
    busy_wait();
    command(POF); // turn off
  }

}
